<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\annotations\builder;

#use \InvalidArgumentException;
#use tehframework\annotations\TagDefinition;

class TagBuilderImpl extends TagBuilder
{
	const TAG_CLASS_FORMAT = '%sTag';
	
	/**
	 * @var bool Flag indicating whether to use autoload of tag classes.
	 */
	private $autoload;
	
	/**
	 * @var array<string, string>
	 */
	private $tagToClassMap = array();
	
	/**
	 * @var array<string>
	 */
	private $namespaces = array();
	
	/**
	 * @var string? A name of the class used for unknown tags.
	 */
	private $unknownTagClass;
	
	/**
	 * Constructs a new default DocBlock tag builder.
	 * 
	 * @param  bool $autoload
	 *         Whether to use autoload of tag classes.
	 * @param  array<string, string>? $tagToClassMap
	 *         An array mapping tag names to class names.
	 * @param  string? $unknownTagClass
	 *         A name of the class used for unknown tags.
	 */
	public function __construct(
		$autoload = false,
		array $namespaces = null,
		array $tagToClassMap = null,
		$unknownTagClass = null)
	{
		$this->autoload        = (bool)$autoload;
		$this->tagToClassMap   = (array)$tagToClassMap;
		$this->namespaces      = (array)$namespaces;
		
		if (!empty($unknownTagClass) && is_string($unknownTagClass))
		{
			$this->unknownTagClass = $unknownTagClass;
		}
	}
	
	/**
	 * Enables autoload of tag classes.
	 * 
	 * @return TagBuilderImpl
	 */
	public function enableAutoload()
	{
		$this->autoload = true;
		
		return $this;
	}
	
	/**
	 * Disables autoload of tag classes.
	 * 
	 * @return TagBuilderImpl
	 */
	public function disableAutoload()
	{
		$this->autoload = false;
		
		return $this;
	}
	
	/**
	 * Maps a tag name to a class name.
	 * 
	 * @param  string $tagName
	 *         A name of the tag.
	 * @param  string $className
	 *         A name of the existing and already loaded class.
	 * @return TagBuilderImpl
	 * @throws InvalidArgumentException
	 *         If the specified tag name is not a string or is empty.
	 * @throws InvalidArgumentException
	 *         If the specified class could not be found without autoloading.
	 */
	public function mapTagToClass($tagName, $className)
	{
		if (empty($tagName) || !is_string($tagName))
		{
			throw new InvalidArgumentException(
				'Tag name must be a non-empty string.');
		}
		
		if (!class_exists($className, false))
		{
			throw new InvalidArgumentException(sprintf(
				'Can not map [%s] tag to non-existing class: [%s]. ' .
				'Forgot to load it?', $tagName, $className
			));
		}
		
		$this->tagToClassMap[strtolower($tagName)] = $className;
		
		return $this;
	}
	
	/**
	 * Maps the specified tag names to class names.
	 * 
	 * @param  array<string, string> $tagToClassMap
	 * @return TagBuilderImpl
	 * @throws InvalidArgumentException
	 *         If any tag name in the specified array is not a string
	 *         or is empty.
	 * @throws InvalidArgumentException
	 *         If any class in the specified array could not be found.
	 */
	public function mapTagsToClasses(array $tagToClassMap)
	{
		foreach ($tagToClassMap as $tagName => $className)
		{
			$this->mapTagToClass($tagName, $className);
		}
		
		return $this;
	}
	
	/**
	 * Adds a new namespace where tag classes can be found.
	 * 
	 * @param  string $namespaceName
	 * @return TagBuilderImpl
	 */
	public function addNamespace($namespaceName)
	{
		$this->namespaces[] = (string)$namespaceName;
		
		return $this;
	}
	
	/**
	 * Adds namespaces where tag classes can be found from the specified array.
	 * 
	 * @param  array<string> $namespaceNames
	 * @return TagBuilderImpl
	 */
	public function addNamespaces(array $namespaceNames)
	{
		foreach ($namespaceNames as $namespaceName)
		{
			$this->addNamespace($namespaceName);
		}
		
		return $this;
	}
	
	/**
	 * Defines a class for unknown tags.
	 * 
	 * @param  string $unknownTagClass
	 *         A name of the already loaded class for unknown tags.
	 * @return TagBuilderImpl
	 * @throws InvalidArgumentException
	 *         If the specified class could not be found without autoloading.
	 */
	public function setClassForUnknownTags($unknownTagClass)
	{
		if (!class_exists($unknownTagClass, false))
		{
			throw new InvalidArgumentException(sprintf(
				'Specified class for unknown tags does not exist: [%s]. ' . 
				'Forgot to load it?', $unknownTagClass
			));
		}
		
		$this->unknownTagClass = $unknownTagClass;
		
		return $this;
	}
	
	/**
	 * Returns an instance of Tag built from the specified definition.
	 *
	 * @param  TagDefinition $tagDefinition
	 *         A definition of tag to build.
	 * @return Tag
	 *         An instance of Tag from the specified definition.
	 * @throws BuilderException
	 */
	public function buildTag(TagDefinition $tagDefinition)
	{
		$class = new ReflectionClass($this->resolveClassName($tagDefinition->name));
		
		if ($class->isSubclassOf('Tag') === false)
		{
			throw new BuilderException(sprintf(
				'[%s] can not be used for [%s] tag, ' . 
				'because it does not implement Tag interface.',
				$class->getName(), $tagDefinition->name
			));
		}
		
		return $class->newInstance($tagDefinition);
	}
	
	/**
	 * @param  string $tagName
	 * @return string
	 * @throws BuilderException
	 */
	private function resolveClassName($tagName)
	{
		if (isset($this->tagToClassMap[$tag = strtolower($tagName)]))
		{
			return $this->tagToClassMap[$tag];
		}
		
		$class = sprintf(self::TAG_CLASS_FORMAT, ucfirst($tagName));
		
		if (class_exists($class, $this->autoload))
		{
			return $class;
		}
		
		foreach ($this->namespaces as $ns)
		{
			if (class_exists($ns . T_NS_SEPARATOR . $class, $this->autoload))
			{
				return $ns . T_NS_SEPARATOR . $class;
			}
		}
		
		if ($this->unknownTagClass !== null)
		{
			return $this->unknownTagClass;
		}
		
		throw new BuilderException(sprintf(
			'Can not find a class for [%s] tag.', $tagName
		));
	}
}